Docs V2: Supabase integration section + canonical encryptedSupabaseV3 reference - #39
Docs V2: Supabase integration section + canonical encryptedSupabaseV3 reference#39coderdan wants to merge 30 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…reference (CIP-3328)
- /reference/stack/supabase: THE canonical encryptedSupabase page —
one signature ({encryptionClient, supabaseClient}, .from(table,
schema)), full query-builder surface, schema-capability → EQL-term
mapping, response/error shapes, deferred-execution model
- /integrations/supabase: flagship tutorial rewritten on EQL v3
(install script → eql_v3 typed columns → wrapper → queries incl.
ORDER BY on _ord; free-text as token containment, no LIKE)
- /integrations/supabase/{database,auth,dashboard-experience}:
migrations-vs-SQL-Editor install, grants, indexes, RLS composition;
Supabase Auth JWT → LockContext; Table Editor expectations + OAuth
integration flow
- placeholder pages for 6 forward-linked IA URLs (identity-aware-
encryption, schema-design, encrypt-existing-data, drizzle,
audit-logging, cts) so no internal link 404s
- IA.md: tick CIP-3328 items; add CIP-3355 SDK-alignment gate
(wrapper wire behaviour is still EQL v2 — blocks final merge)
9683c86 to
fd918a6
Compare
Reorganize /integrations/supabase from 4 pages into: Overview (front
door from the marketplace listing), Quickstart (the tutorial, OAuth
folded in as optional), Fundamentals (database.mdx + dashboard-
experience.mdx merged), Data layer (ORM/framework router), Auth
(rebuilt on OIDC federation), and Edge Functions (new).
- Auth: replace the deprecated LockContext.identify() flow with
OidcFederationStrategy as config.authStrategy, then
.withLockContext({ identityClaim }) — lock context now correctly
documented as requiring federation.
- Data layer: document the PostgREST vs direct-Postgres fork and
Supabase pooler/connection glue; link out to canonical ORM pages
rather than duplicating them.
- Edge Functions: written on the @cipherstash/stack/wasm-inline
surface; wrapper-on-edge question gated behind a callout.
- Add Prisma + Next.js integration placeholders so Data-layer links
resolve; fix stale /supabase/database link in the stack reference.
- Reorder meta.json; remove merged pages.
Addresses Vercel preview comment: the section folder and the index page were both labelled 'Supabase', so the sidebar read 'Supabase › Supabase'. Title the index page 'Overview' to remove the repeat.
Replace the "How it fits together" section on the Supabase overview with launch-article "How it works" copy. Fix EQL domain names against eql_v3 (int_ord -> integer_ord) and reframe text_search match as containment matching (like LIKE '%term%'), with a caveat callout. Add Table Editor screenshot.
- Remove em-dashes across the overview (AI "tell") - Backtick `base` in "EQL base types" - Convert the text_search note to a concise <Callout> - Collapse the RLS/stack/KMS/performance/security sections into one linked "Built for the Supabase stack" summary
Add a shared, product-wide FAQ component (src/components/faq) that renders an accordion and emits FAQPage JSON-LD. Generic answers (privacy, scaling, KMS, RLS-vs-encryption, free tier) live once in faq/shared and are composed with Supabase-specific questions inline on the overview. De-dupe the performance/privacy prose now covered by the FAQ.
Reuse the shared privacy/KMS/free-tier FAQ entries and add DynamoDB-specific questions (equality-only querying, keeping AWS SDK control, adoption). Omits the Postgres-oriented scaling/RLS entries, which do not apply.
# Conflicts: # src/mdx-components.tsx
…ains
The Supabase pages documented `encryptedSupabase`, the EQL v2 dialect.
Stack 1.0 ships `encryptedSupabaseV3` alongside it, and it is a different
shape. Verified against the adapter source on the release branch, not
against the PR descriptions, which have drifted.
- `await encryptedSupabaseV3(url, key, options?)` or `(client, options?)`.
It is async because it introspects the database at connect time, reads
the EQL domain of every column, and derives the encryption config from
it. There is no schema to declare and no `encryptedTable` to keep in
sync, so `.from()` takes only a table name.
- Introspection needs a direct Postgres connection: `DATABASE_URL`, or
`options.databaseUrl`. Documented in the quickstart env block.
- `schemas` is optional, and buys compile-time types plus startup
verification. Import from `@cipherstash/stack/eql/v3`.
- `select('*')` works, expanded from the introspected column list. On v2
it throws.
- `.like()` / `.ilike()` throw on encrypted columns. Free-text search is
`.contains()`. With the default match options the query term carries
the whole query string as a token, so a substring matches nothing;
called out where it will bite.
- Column types are the EQL 3.0.0 prefixed domains: `public.eql_v3_text_eq`,
not `eql_v3.text_eq`. The domains live in `public`; only the functions
and operators live in `eql_v3`.
- Supabase runs the installer as a non-superuser, so the ORE-backed
domains are disabled and raise on first write. Say so on the pages that
offer the choice, and point at `_ord` / `text_search`.
- The query builder's `.withLockContext()` takes a `LockContext`
instance, unlike the core client's, which takes a plain object. The
auth page passed the wrong one.
The v2 wrapper is unchanged and still documented, at the foot of the
reference page.
|
Pushed two commits: a Everything below was verified against the adapter source on
What changed
Two things worth a second opinion
Depends on
Checks
|
The operand is a storage payload, not a query term: PostgREST cannot cast a filter value to the narrow _query domain, and protect-ffi 0.28 rejects encryptQuery for the v3 scalars. So the needle's bloom carries the whole needle as an include_original token, which the haystack's bloom cannot contain unless the two are equal, or the needle is exactly one trigram long. Says it is a defect to be fixed in EQL rather than intended behaviour.
|
Correction to my note above on You're right about the storage side. What I missed is which payload the wrapper puts on the query side. It sends a full storage payload as the operand, not a narrow query term, and the source says why:
Confirmed at So the needle gets tokenized as if it were a stored value: It follows that The class doc reaches the same conclusion and calls it a defect:
The docs now describe it as a known defect with that precise rule, rather than as intended behaviour. I could not find the tracking issue in |
Drizzle and the Supabase wrapper share the defect but not the cause. The common blocker is that protect-ffi has no v3 scalar encryptQuery, so no client can build a narrow operand. Only Supabase is additionally blocked by PostgREST's inability to cast a filter value to the query domain; Drizzle emits its own SQL and can cast once a narrow term exists.
Not a protect-ffi gap: substring containment works at the ffi layer when include_original is off, which is the schema default. stack's shared match defaults force it on, inherited from the v2 builder, where QueryMode dropped the original token. Link the tracking issue.
The Vercel build failed in `generate-docs`. Three causes, all from the Stack 1.0 monorepo restructure: 1. Stale entry points. `secrets/`, `drizzle/`, and `supabase/` no longer exist under `packages/stack/src` (secrets removed; the two adapters moved to their own packages), so TypeDoc had nothing to read. Dropped them; kept the seven modules `@cipherstash/stack` still ships. 2. Type errors from cross-package resolution. The stack monorepo is a pnpm workspace, but the generator installs it with `bun`, which leaves references like `@cipherstash/protect-ffi`'s `ProtectError` unresolved for the isolated typecheck even though 0.29.0 exports it. Set TypeDoc's `skipErrorChecking: true` — it emits accurate signatures from the source AST regardless, which is the right posture for documenting external source we don't control. 3. Multi-version generation broke on the layout change. The module layout diverged across majors (0.18 still has the adapters/secrets; 1.0 does not), and one shared `entryPoints` config can't document both. Generate only the latest major. Also reset the working tree before each checkout, since a prior tag's `bun install` rewrites the tracked package.json and would block switching tags. Also strips the now-dead TypeDoc deep-links from the legacy Drizzle reference page (the adapter's API pages are no longer generated here) and points it at /integrations/drizzle. Verified: `generate-docs`, `validate-links`, and a full `bun run build` all pass.
|
Fixed the failing Vercel build (
Verified locally: This is a shared generator fix. #43 and #54 will hit the same red build until they get it — either after this merges to v2 and they rebase, or I can cherry-pick |
# Conflicts: # scripts/lib/docs-generator.ts # src/mdx-components.tsx
Stack renamed the encrypted free-text method in cipherstash/stack#617 (`feat(stack-drizzle,stack-supabase)!: rename encrypted contains() -> matches()`), landed 2026-07-13 — one day before this branch was last updated, so it was missed. The rename is not cosmetic. `contains()` now RAISES on an encrypted text column, because containment promises exactness that bloom-filter matching cannot deliver: contains() is native (exact) containment and does not apply to encrypted column "email". Use matches() for encrypted free-text search. Every free-text example on these pages taught the spelling that now raises. `contains()` is still correct for encrypted JSON (sub-document containment) and plaintext arrays, so the filter table now lists both, plus the selectorEq/selectorNe pair added alongside them. Also rewrites "There is no `.like()`", which was wrong in the other direction. `like`/`ilike` on an encrypted column do not throw — they are a compatibility shim that reduces the pattern to a needle and delegates to `matches()`. The result is approximate (case-insensitive, one-sided, anchoring lost) and only an unapproximable pattern — an internal `%`, or any `_` — raises. Plaintext columns keep real SQL LIKE. The section now says that, and the anchor is renamed to match, with the inbound link updated. Verified against @cipherstash/stack@1.0.0-rc.4 (packages/stack-supabase): encryptedSupabaseV3's four overloads are async and take (client | url+key) plus options, and databaseUrl/config/schemas are all real, so the surrounding examples were already correct. The v2 paragraph checks out too — encryptedSupabase is synchronous, takes { encryptionClient, supabaseClient }, and needs .from(table, schema). Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
|
Rebased onto current Conflicts (2, both straightforward)
API drift — one breaking rename, missed by one day
It is not cosmetic. All six free-text examples and prose references taught the spelling that now raises. Fixed in "There is no
|
… adapter Correcting my own earlier commit. Renaming contains() -> matches() fixed the spelling but documented an operation that does not run: on EQL 3.0.2+ the supabase-js wrapper raises for every encrypted free-text and encrypted-JSON term, whatever the spelling. [supabase v3]: matches() on encrypted column "email" is unavailable with EQL 3.0.2+: the SQL operator requires an eql_v3.query_* cast that PostgREST cannot express. Use the Drizzle or Prisma Next adapter, or a scoped SQL/RPC function. The docs pin EQL 3.0.4, so this is the behaviour every reader gets. The guard fires for the freeTextSearch and searchableJson query types via any spelling — matches(), contains() on a json column, selectorEq()/selectorNe(), and the same terms reached through not(), or(), or a raw filter() — and fails closed on an unknown EQL version. It does NOT touch equality, ordering, or range, so the rest of the page stands. Scoped to the supabase-js query path, which is what the limit is about. The Auth and Edge Functions pages are unaffected: this is PostgREST's inability to express an operand cast, not a limit on CipherStash with Supabase. - The wrapper reference leads the free-text section with the constraint and the three workarounds, and marks the affected rows in the filter table. - The quickstart drops its free-text example rather than showing a call that raises, and its callout now says which operations do work. - fundamentals' column-type table is accurate as written (these ARE the column's capabilities) but a reader could pick text_match or json and only then discover they cannot query it here, so it points at the constraint. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
|
Synced to current Correcting my earlier commit
These docs pin EQL 3.0.4, so that's what every reader gets. The guard fires for the It does not touch equality, ordering, or range. Scoped deliberately to the supabase-js query path — this is PostgREST's inability to express an operand cast, not a limit on CipherStash with Supabase. Auth and Edge Functions are untouched.
Note for reviewThe
|
…s remove_v2 Two Vercel toolbar comments on the preview. edge-functions: generate the CS_* values with `stash env` rather than having the reader assemble the file by hand. Additive — the explicit variable listing stays, since it is what documents which four the SDK actually reads. `--write` is a boolean (verified against stash@0.17.1), so the example redirects to the env file that `supabase functions serve --env-file` then consumes. fundamentals: rewrite the install callout against the stack `remove_v2` branch. What was out of date: - It justified the v3 restrictions as flags that "exist for the v2 install paths". With v2 removed there are no v2 install paths, and the install is always v3, so the framing goes. - It listed `--drizzle` alongside the other rejected flags. `--drizzle` has had its own v3 answer since stack#691: `stash eql migration --drizzle` generates a v3 install migration, cs_migrations tracking schema included, for Drizzle projects that want the install in their migration history. The install command still rejects the flag, but now points there rather than at `--eql-version 2`. - Also drops "`--eql-version 2` opts back to v2" from the prose above it. `--migration`, `--migrations-dir`, and `--latest` remain rejected, and there is still no Supabase migration variant — one v3 SQL artifact targets every platform — so the `supabase db reset` consequence stands and is kept. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
|
Both Vercel toolbar comments addressed in
A bigger decision this turned up
Two consequences for this PR:
I haven't done the rename, because of a conflict worth your call: the generated reference builds from the latest published tag, which is Options as I see them:
Happy to do whichever. The rename itself is mechanical; deleting or rewriting the v2-wrapper section is the only part needing judgement. |
|
Correcting my previous comment: /**
* @deprecated Use {@link encryptedSupabase}. `encryptedSupabaseV3` is a
* type-identical alias kept for existing imports; the `V3` suffix is redundant
* now that EQL v3 is the only generation this wrapper authors.
*/
export const encryptedSupabaseV3 = encryptedSupabaseThe types are aliased the same way — So there is no rename deadline, and no window of contradiction. Every What I verified, and how — usable now, without waiting for a tag
PROTECT_WORKSPACE_PATH=/path/to/stack-remove-v2 bun run generate-docsThat ran clean — 0 TypeDoc errors — and produced The one thing that IS wrong at launchThe "The v2 wrapper" section. That's a small, self-contained edit: delete the section and drop the "Pick the dialect by function" framing, since after the merge there is only one. Happy to do it now — it's correct against Also worth noting from the same check: the EQL 3.0.2+ free-text/JSON guard survives on |
…stead `remove_v2` makes `encryptedSupabase` the v3 wrapper, so a section documenting it as "the EQL v2 dialect" would describe the opposite of what it names. The v2 dialect it described will never have existed for a reader of the live site, since these docs ship after that branch. Replaced with a short note on the naming, because the alias is the thing a reader actually needs: `encryptedSupabase` and `encryptedSupabaseV3` are the same function, either import works, and the V3 spelling is a deprecated alias kept for existing imports now that the suffix is redundant. Same for the type names. Also drops the "pick the dialect by function" framing — after the merge there is only one dialect. The examples on this page still say `encryptedSupabaseV3`, deliberately. That name is correct both today and after the merge, whereas `encryptedSupabase` means the OLD synchronous v2 wrapper on the currently published SDK (1.0.0-rc.4) — switching the examples now would document a different function with a different signature. They should be renamed once remove_v2 ships, along with this page's `verifiedAgainst`. Also fixes one cell in choose-your-stack, which is outside this PR's scope but factually wrong in the same way: it advertised `.like()` as something that "keeps working" on Supabase. It delegates to `matches()`, which raises on EQL 3.0.2+ through PostgREST. Swapped for `.order()`, which does. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
|
v2-wrapper section fixed in Removed the section documenting Replaced with a short note on the naming, since the alias is what a reader actually needs: the two names are the same function, either import works, and Why the examples still say
|
1.0.0-rc.4 (published) |
after remove_v2 |
|
|---|---|---|
encryptedSupabase |
v2 wrapper — synchronous, { encryptionClient, supabaseClient }, .from(table, schema) |
v3 wrapper, async |
encryptedSupabaseV3 |
v3 wrapper, async | same function, deprecated alias |
Renaming the examples now would point readers at a different function with a different signature on the SDK that's actually published. So the page keeps the spelling that's true in both worlds, and the note explains the discrepancy rather than pretending it away.
Follow-up when remove_v2 ships: rename the examples to encryptedSupabase across this page, the quickstart, auth, edge-functions and the section index, and update this page's verifiedAgainst (currently stack: 1.0.0-rc.0, already behind). Mechanical, and verifiable before the tag exists with PROTECT_WORKSPACE_PATH=/path/to/stack-remove-v2 bun run generate-docs.
One out-of-scope fix, flagged
get-started/choose-your-stack.mdx isn't part of this PR, but its Supabase row advertised .like() as something that "keeps working". It delegates to matches(), which raises on EQL 3.0.2+ through PostgREST — the same error I documented elsewhere in this PR, on a getting-started page. Swapped .like() for .order(), which does work. One cell; happy to split it out if you'd rather keep this PR clean.
Left alone, needs a decision
reference/agent-skills.mdx describes the stash-supabase skill as covering filters "eq, neq, like, ilike, gt, …" — no matches, no contains/selectorEq. That list looks stale against the v3 surface, but it documents what the skill covers rather than teaching the API, and checking it properly means reading the ~800-line skills/stash-supabase/SKILL.md on remove_v2 and confirming what it actually teaches. Worth doing, but it's a separate pass — say the word and I'll take it.
bun run build green: no broken links, all validators pass.
… Data layer away Four structural changes to the Integrations section: - The /integrations landing page leaves the sidebar (`"!index"` in meta.json, which drops it as both a child item and the folder link). It stays reachable from the docs index, Get started, and the v2 redirect map — it just isn't a nav entry. - The landing page becomes a plain list by category: Database (Supabase, Prisma Postgres) and ORM (Supabase-js, Prisma Next, Drizzle). The decision content it used to duplicate — the Postgres platform table, the ORM table, the identity-provider table — already lives on /get-started/choose-your-stack, which it now points at instead. - Supabase Overview and Fundamentals merge into one Overview page. The overlap was substantial: both explained EQL column types, both covered RLS, and both explained why the Table Editor can't create encrypted columns. Fundamentals' section anchors are preserved on the merged page, so #installing-eql, #row-level-security, #working-in-the-dashboard and #connecting-your-project-to-cipherstash keep resolving; inbound links across auth, quickstart and the wrapper reference are repointed. - Data layer folds into the Quickstart as "Using an ORM instead", condensed from 71 lines to ~20: what the direct-connection path is, which Supabase connection string to use, and the fact that a privileged direct connection bypasses RLS. Verified against the rendered output rather than the source: the built sidebar has no bare /integrations link, and every anchor the merge moved resolves to a real id on the merged page. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
|
Pushed 1. 2. Landing page is now a list by category. Database (Supabase, Prisma Postgres) and ORM (Supabase-js, Prisma Next, Drizzle), and not much else. Two link choices worth a look, since neither entity had a page of its own:
I also dropped three tables from that page — Postgres platforms, ORMs, identity providers. They weren't lost: all three are already on 3. Overview + Fundamentals merged. You were right that they overlapped: both explained EQL column types (Fundamentals' table is better, so that's the one that survived), both covered RLS, and both explained the Table Editor's custom-domain problem. Net −130 lines against a straight concatenation. The four Fundamentals anchors are preserved on the merged page, so 4. Data layer folded into the Quickstart as How I checked it. Then I checked the built HTML rather than the source: no bare One thing to note: Two things I left alone, deliberately:
Say the word on either and I'll pick them up. |
`content/stack/reference/drizzle.mdx` points at `/integrations/drizzle`, which is a page this branch creates. #81 (the link-validator fix, off v2) removes that link, because on v2 alone it is a genuine 404 — the target does not exist there yet. That set up a silent failure. This branch did not touch the line, so rebasing onto a v2 containing #81 would have taken #81's removal without a conflict, dropping the cross-link even though the page it points at had just landed. Rewording the sentence here makes the branch an explicit editor of that line, so the two changes conflict and the merge has to make a decision instead of quietly picking one. The rewrite is also more accurate on its own terms: this page documents the older `@cipherstash/stack/drizzle` entry point, which the previous phrasing left implicit. The redirect itself needed no change — `/stack/reference/drizzle` → `/integrations/drizzle` has been in v2-redirects.mjs all along. What this branch supplies is the destination, so the redirect resolves here where it did not on v2. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
|
Pushed The redirect. The problem worth fixing. I verified both halves of that with
So the merge now has to make a decision. The reword is also more accurate on its own terms — this page documents the older
On merge order, either way works now:
Build is green at 785 pages, |
# Conflicts: # content/stack/reference/drizzle.mdx
Supabase integration section, plus the canonical
encryptedSupabaseV3reference. Targets thev2docs branch.Originally written against
encryptedSupabase(the EQL v2 dialect). Reworked ontoencryptedSupabaseV3and the EQL 3.0.0 domains, verified against the adapter source onfeat/eql-v3-text-search-schemaand against the EQL v3 domain SQL rather than the PR descriptions, which have drifted.Pages
/reference/stack/supabase— the canonical wrapper page.await encryptedSupabaseV3(url, key, options?)or(client, options?), connect-time introspection,.from(tableName), optionalschemasfor types and startup verification, the full builder surface, the domain-to-term table, response and error shapes, and the deferred-execution model. The v2 wrapper is documented at the foot./integrations/supabase— section overview./integrations/supabase/quickstart— empty project to encrypted, searchable table: install EQL, type the columns, wrap the client, query./integrations/supabase/fundamentals— migration vs SQL Editor install, role grants, functional indexes, RLS composition, pooler and introspection connections./integrations/supabase/auth— Supabase Auth session toOidcFederationStrategy, then.withLockContext()./integrations/supabase/orms— PostgREST path vs a direct Postgres connection./integrations/supabase/edge-functions— thewasm-inlinecore client on the Edge runtime.What the rework changed
encryptedTableto keep in sync, and.from()takes only a table name.DATABASE_URLoroptions.databaseUrl), a different credential from the anon key. Added to the quickstart env block.schemasis optional: compile-time types plus startup verification, imported from@cipherstash/stack/eql/v3. There is no@cipherstash/stack/schema/v3export.select('*')works on v3, expanded from the introspected column list. It throws on v2.public.eql_v3_text_eq, noteql_v3.text_eq. Domains live inpublic; functions and operators live ineql_v3..like()/.ilike()throw on encrypted columns. Free-text search is.contains()..withLockContext()takes aLockContextinstance; the core client's takes a plain{ identityClaim }object. The auth page was passing the core shape.Known defect, documented
.contains()cannot match substrings today:text_searchforcesinclude_original: true, so the needle's bloom carries the whole needle as an extra token that the haystack's bloom cannot contain. Tracked in cipherstash/stack#610. A warn callout describes the behaviour; remove it when the fix lands.Depends on
encryptedSupabaseV3rejects every EQL 3.0.0 table, becauseDOMAIN_REGISTRYis keyed on the old unprefixed names..in()on encrypted columns and.not(col, 'in', […]), both documented as working in the filter table.Notes for review
/reference/eql/*; these pages link rather than restate.edge-functions.mdxstill uses the v2 schema builders, because@cipherstash/stack/wasm-inlineexportsencryptedTable/encryptedColumnfrom@/schemaand does not export the v3types. Left alone; it already carries a "being confirmed" callout. Worth deciding whether the edge build should re-export the v3 surface.v2branch and are tracked separately; they must be stripped before anything reaches production.bun run buildpasses.validate-content,validate-links,validate-mermaidpass.